/**
 * Copyright (c) 2010 Pablo Muñoz Galindo
 * <p>
 * Este software está licenciado bajo los términos de la licencia GNU GPLv3
 * <br>
 * Para más información:
 * <a href="http://www.gnu.org/licenses/gpl-3.0-standalone.html"> http://www.gnu.org/licenses/gpl-3.0-standalone.html </a>
 * <br>
 * Este software es distribuido "como es", no ofreciendo garantías de ningún tipo. La responsabilidad
 * de su uso queda bajo la cuenta y riesgo del propio usuario.
 *
 */
package esteganografo.core;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.bouncycastle.crypto.digests.MD5Digest;

/**
 * Clase para encriptar un mensaje de texto con AES-128 a partir de una contraseña
 * y ocultar el resultado en un fichero
 *
 * @author Pablo Muñoz Galindo
 * @version 0.1b
 */
public class EncoderConAES {

    /**
     * Fichero original
     */
    public String fichero_orig;
    /**
     * Fichero de destino que portará la información
     */
    public String fichero_dest;

    /**
     * Constructor con el que se selecciona el fichero de origen y de destino
     * @param fichero_orig
     * @param fichero_dest
     */
    public EncoderConAES(String fichero_orig, String fichero_dest) {
        this.fichero_orig = fichero_orig;
        this.fichero_dest = fichero_dest;
    }

    /**
     * Método para encriptar un mensaje con AES-128 y ocultarlo dentro de un fichero
     *
     * @param mensaje Mensaje a ocultar
     * @param password Contraseña con la que se encriptará el mensaje
     * @throws Exception Si hay problemas de encriptación o de entrada/salida
     */
    public void encode(String mensaje, String password)
            throws Exception {

        //Generamos y guardamos en un fichero la suma del mensaje
        OutputStream out_md5 = new FileOutputStream(fichero_dest + ".md5");
        MD5Digest md5 = new MD5Digest();
        byte[] bytes = mensaje.getBytes();
        md5.update(bytes, 0, bytes.length);
        byte[] suma = new byte[md5.getDigestSize()];
        md5.doFinal(suma, 0);
        out_md5.write(suma);
        out_md5.close();

        //Encriptamos el contenido del mensaje
        String mensajeEncriptado = Utilities.cifraConAES128(mensaje, password);

        //ocultamos el resultado en el fichero
        encode(mensajeEncriptado);
    }

    /**
     * Método para infiltrar el mensaje dado en el fichero
     *
     * @param mensaje Mensaje a ocultar
     * @throws IOException si hay problemas de entrada/salida
     */
    public void encode(String mensaje)
            throws Exception {
        InputStream in = new FileInputStream(fichero_orig);
        OutputStream out = new FileOutputStream(fichero_dest);

        String messageBytes = stringToBinaryString(mensaje);

        //Se crea un marcador con el número de bits que se van a codificar
        String markerBytes = Long.toBinaryString((long) mensaje.length());
        while (markerBytes.length() < 64) {
            markerBytes = '0' + markerBytes;
        }

        //Leemos y escribimos la cabecera
        byte[] header = new byte[53];
        in.read(header, 0, 53);
        out.write(header);

        //Escribimos el número de caracteres que se va a ocultar
        byte[] marker = new byte[markerBytes.length()];
        in.read(marker);
        for (int i = 0; i < marker.length; i++) {
            marker[i] = modifyByte(marker[i], Integer.parseInt("" + markerBytes.charAt(i), 2));
        }
        out.write(marker);

        //Escribimos el mensaje
        byte[] message = new byte[messageBytes.length()];
        in.read(message);
        for (int j = 0; j < messageBytes.length(); j++) {
            message[j] = modifyByte(message[j], Integer.parseInt("" + messageBytes.charAt(j), 2));
        }
        out.write(message);

        //Escribimos el resto del fichero sin alterar
        byte[] buffer = new byte[256];
        while (true) {
            int n = in.read(buffer);
            if (n < 0) {
                break;
            }

            out.write(buffer, 0, n);
        }

        //Cerramos los ficheros abiertos
        in.close();
        out.close();
    }

    /**
     * Método privado para convertir una cadena en su String equivalente en binario
     */
    private static String stringToBinaryString(String cadena) {
        String resultado = "";
        for (int i = 0; i < cadena.length(); i++) {
            String caracter = Integer.toBinaryString((int) cadena.charAt(i));
            int dim = caracter.length();
            if (dim < 8) {
                for (int j = 0; j < 8 - dim; j++) {
                    caracter = '0' + caracter;
                }
            }
            resultado += caracter;
        }
        return resultado;
    }

    /**
     * Método privado para alterar un byte
     */
    private byte modifyByte(byte b, int value)
            throws Exception {
        if (value != 0 && value != 1) {
            throw new Exception("value no representa un caracter binario");
        }
        //Procedimiento alternativo al comentado
        return (byte) ((b & 0xFE) | value);
    }
}
